前言
在基于Vue 项目的开发过程中,有时候需要引用一些全局文件(定义了样式变量、方法等),这么做是为了增加复用性,提高开发和多人协作的效率。如果是小型或者引用数量比较少的项目,这种全局文件可以直接进行引用;但是在大型项目中,如果每一个需要到的地方都要 import 一次,无疑会增加项目的耦合程度,一旦全局文件的维护者更改了文件名或路径,那便会产生灾难性的影响,影响面非常广。
对于大型的项目,我们还是希望关注点分离,让开发业务代码的同学只关心调用。Vue-CLI3 生成的项目中,官方在文档中提供了几种通过修改 Webpack Loader 配置引入全局文件的方法,分别为:
通过 chainWebpack 选项(style-resources-loader);
通过 Vue-CLI Plugin(vue-cli-plugin-style-resources-loader);
通过 css.loaderOptions 选项;
下面我们以 SCSS 为例,介绍下这几种引入方法;同时以源码的角度,看看导入全局样式文件后,脚手架内部发生了什么。
chainWebpack 选项
vue.config.js 中提供了直接修改 Webpack 配置的选项,主要是通过 webpack-chain 这个库实现的,它提供了抽象化的链式调用方法并生成配置,可以通过该选项,向预处理器 Loader 导入全局文件。这里主要是通过 style-resources-loader 的 options.patterns 选项,向 Webpack 传递全局文件,先看一段示例代码:

Vue-CLI3 中样式相关配置的生成,主要位于 @vue/cli-service/lib/config/css.js 中,该文件的作用主要是生成样式相关的配置,并在 @vue/cli-service/lib/Service.js 中被调用,以下是部分关键源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| function createCSSRule (lang, test, loader, options) { const baseRule = webpackConfig.module.rule(lang).test(test)
const vueModulesRule = baseRule.oneOf('vue-modules').resourceQuery(/module/) applyLoaders(vueModulesRule, true)
const vueNormalRule = baseRule.oneOf('vue').resourceQuery(/\?vue/) applyLoaders(vueNormalRule, false)
const extModulesRule = baseRule.oneOf('normal-modules').test(/\.module\.\w+$/) applyLoaders(extModulesRule, true)
const normalRule = baseRule.oneOf('normal') applyLoaders(normalRule, modules)
function applyLoaders (rule, modules) { ... } }
createCSSRule('css', /\.css$/) createCSSRule('postcss', /\.p(ost)?css$/) createCSSRule('scss', /\.scss$/, 'sass-loader', loaderOptions.sass) createCSSRule('sass', /\.sass$/, 'sass-loader', Object.assign({ indentedSyntax: true }, loaderOptions.sass)) createCSSRule('less', /\.less$/, 'less-loader', loaderOptions.less) createCSSRule('stylus', /\.styl(us)?$/, 'stylus-loader', Object.assign({ preferPathResolver: 'webpack' }, loaderOptions.stylus))
|
在该文件中,CSS 相关的规则会先在 createCSSRule 这个方法被生成,它支持传入四个参数,分别为:1. 创建的规则名;2. 匹配特定的正则;3. 使用的自定义 Loader;4. 对应的Loader 配置。
在函数内部,上文提到的 vue-modules、vue、normal-modules、normal 四种嵌套的匹配规则,则是对应了几种样式被引用的方式,这里使用的是 Rule.oneOf 这个 API ,所以匹配的优先级是从上往下降级;在规则被创建完以后,则会调用 applyLoaders 这个方法,我们对其简单地解析下(省略部分逻辑):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| function applyLoaders (rule, modules) { if (shouldExtract) { rule .use('extract-css-loader').loader(require('mini-css-extract-plugin').loader) .options({ publicPath: cssPublicPath }) } else { rule.use('vue-style-loader').loader('vue-style-loader').options({ sourceMap, shadowMode }) }
const cssLoaderOptions = Object.assign({ sourceMap, importLoaders: ( 1 + (hasPostCSSConfig ? 1 : 0) + (needInlineMinification ? 1 : 0) ) }, loaderOptions.css)
if (modules) { const { localIdentName = '[name]_[local]_[hash:base64:5]' } = loaderOptions.css || {} Object.assign(cssLoaderOptions, { modules, localIdentName }) }
rule.use('css-loader').loader('css-loader').options(cssLoaderOptions)
if (needInlineMinification) { rule.use('cssnano').loader('postcss-loader').options({ sourceMap, plugins: [require('cssnano')(cssnanoOptions)] }) }
if (hasPostCSSConfig) { rule.use('postcss-loader').loader('postcss-loader').options(Object.assign({ sourceMap }, loaderOptions.postcss)) }
if (loader) { rule.use(loader).loader(loader).options(Object.assign({ sourceMap }, options)) } }
|
在 CSS Modules 相关的配置中,只有在 vue.config.js 中将 css.modules 设置为 true,或者按照 Vue-CLI 约定的方式进行引入才会被开启。
可以看到,createCSSRule 方法已经将大部分关于样式的规则预先创建好,其内部 applyLoaders 方法则会对配置项进行创建与合并,并传递至对应的 Loader。
把代码加上注释便于理解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const path = require('path')
module.exports = { chainWebpack: config => { const types = ['vue-modules', 'vue', 'normal-modules', 'normal'] types.forEach(type => { config.module.rule('scss') .oneOf(type) .use('style-resource').loader('style-resources-loader') .options({ patterns: [ path.resolve(__dirname, 'src/style/variables/*.scss') ] }) }) } }
|
解析下这段代码,我们的需求是对 SCSS 的规则里,添加 style-resources-loader ,所以需要找到生成默认配置的。这段例子具有很多有意思的地方,先从审查命令说起:
1
| vue inspect module.rules > output.js
|
通过该命令,可以方便地将 解析出来的 webpack 配置、包括链式访问规则和插件 的提示重定向到 output.js 文件,而这里我们只需要查看 Rules。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| { ... { test: /\.scss$/, oneOf: [ { resourceQuery: /module/, use: [ { loader: 'vue-style-loader', options: { ... } }, { loader: 'css-loader', options: { ... } }, { loader: 'postcss-loader', options: { ... } }, { loader: 'sass-loader', options: { ... } } ] }, {...}, {...}, {...} ] }, ... }
|
在脚手架默认配置,对 SCSS 的处理规则里会对 vue-modules、vue、normal-modules、normal 四种类型依次进行规则匹配,当规则匹配时只使用第一个匹配规则。在每一个嵌套的匹配规则中,再依次使用 vue-style-loader、css-loader、postcss-loader、sass-loader 等 Loader。
将示例代码引入后,再次审查配置,应该会在发现在 use 选项的队列中,新增了一个 Loader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { test: /\.scss$/, oneOf: [ { resourceQuery: /module/, use: [ {...}, {...}, {...}, {...}, { loader: 'style-resources-loader', options: {...} } }, ... ] }
|
官方文档中,同时还提到了 vue-cli-plugin-style-resources-loader ,这是一个标准的 Vue-CLI3 插件。
但是在官方文档中,这种方式配置却是不被推荐的。chainWebpack 选项提供了细颗粒度修改配置的方式,但是对于样式的规则来说,本身默认生成的嵌套规则就已经达到了 4*3 种,通过以上这种方式无疑会增加了配置的复杂度、耦合度,容易造成疏漏、非常不利于维护:
对于 CSS 相关 loader 来说,我们推荐使用 css.loaderOptions 而不是直接链式指定 loader。这是因为每种 CSS 文件类型都有多个规则,而 css.loaderOptions 可以确保你通过一个地方影响所有的规则。